package cn.iocoder.yudao.module.crm.service.clue;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.distributeconfig.CrmClueDistributeConfigRespVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.distributeconfig.CrmClueDistributeConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.clue.vo.poolconfig.CrmCluePoolConfigSaveReqVO;
import cn.iocoder.yudao.module.crm.controller.admin.module.vo.customerlimit.CrmModuleCustomerLimitRespVO;
import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDistributeConfigDO;
import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmCluePoolConfigDO;
import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueDistributeConfigMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmClueMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.clue.CrmCluePoolConfigMapper;
import cn.iocoder.yudao.module.crm.dal.mysql.customer.CrmCustomerPoolConfigMapper;
import cn.iocoder.yudao.module.crm.dal.redis.RedisKeyConstants;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.starter.annotation.LogRecord;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.parameters.P;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static cn.iocoder.yudao.module.crm.enums.LogRecordConstants.*;

/**
 * 线索自动分配规则配置 Service 实现类
 *
 * @author Wanwan
 */
@Service
@Validated
@Slf4j
public class CrmClueDistributeConfigServiceImpl implements CrmClueDistributeConfigService {

    @Resource
    private CrmClueDistributeConfigMapper clueDistributeConfigMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CrmClueMapper clueMapper;

    private static final Integer LIMIT_ON = 1;

    private static final Integer LIMIT_OFF = 0;


    private static final Integer T_ORDER = 1;

    private static final Integer T_LEAST = 2;

    private static final Integer NO_LIMIT = 999999;

    @Override
    public CrmClueDistributeConfigDO getClueDistributeConfigByModule(Long moduleId) {
        String cache = stringRedisTemplate.opsForValue().get(RedisKeyConstants.M_C_T_C + moduleId);
        CrmClueDistributeConfigDO config;
        if (StrUtil.isEmpty(cache)) {
            config = clueDistributeConfigMapper.selectOne(CrmClueDistributeConfigDO::getModuleId, moduleId);
            if (Objects.nonNull(config)) {
                stringRedisTemplate.opsForValue().set(RedisKeyConstants.M_C_T_C + moduleId, JSONUtil.toJsonStr(config));
            }
        } else {
            config = JSONUtil.toBean(cache, CrmClueDistributeConfigDO.class);
        }
        return config;
    }

    @Override
    @LogRecord(type = CRM_CLUE_DISTRIBUTE_CONFIG_TYPE, subType = CRM_CLUE_DISTRIBUTE_CONFIG_SUB_TYPE, bizNo = "{{#poolConfigId}}",
            success = CRM_CLUE_DISTRIBUTE_CONFIG_SUCCESS)
    public void saveCluePoolConfig(CrmClueDistributeConfigSaveReqVO saveReqVO) {
        CrmClueDistributeConfigDO dbConfig = getClueDistributeConfigByModule(saveReqVO.getModuleId());
        CrmClueDistributeConfigDO poolConfig = BeanUtils.toBean(saveReqVO, CrmClueDistributeConfigDO.class);
        if (Objects.nonNull(dbConfig)) {
            clueDistributeConfigMapper.updateById(poolConfig.setId(dbConfig.getId()));
            // 记录操作日志上下文
            LogRecordContext.putVariable("isPoolConfigUpdate", Boolean.TRUE);
            LogRecordContext.putVariable("poolConfigId", poolConfig.getId());

            // 1.2 建立configList 链表缓存
            refreshOne(poolConfig);
            return;
        }else{
            // 2. 不存在，则进行插入
            clueDistributeConfigMapper.insert(poolConfig);
            // 1.2 建立configList 链表缓存
            refreshOne(poolConfig);
            // 记录操作日志上下文
            LogRecordContext.putVariable("isPoolConfigUpdate", Boolean.FALSE);
            LogRecordContext.putVariable("poolConfigId", poolConfig.getId());
        }
        //建立这个规则的缓存信息
        stringRedisTemplate.opsForValue().set(RedisKeyConstants.M_C_T_C + saveReqVO.getModuleId(), JSONUtil.toJsonStr(poolConfig));

    }

    @Override
    public Long getDistributeUser(Long moduleId) {

        CrmClueDistributeConfigDO dbConfig = getClueDistributeConfigByModule(moduleId);
        if (Objects.isNull(dbConfig)) {
            return 0L;
        }
        Long userId = dbConfig.getDefaultOwner();
        // 获取上限配置
        String strUserId = getFromCache(dbConfig.getModuleId(), dbConfig.getType());
        if (StrUtil.isEmpty(strUserId)) {
            return userId;
        }
        Integer limitConfig = dbConfig.getLimitConfig();
        Long decrement = 1L;
        if (ObjectUtil.equal(limitConfig, LIMIT_OFF)) {
            // 不管领取上限， 领取了更新缓存计数
            resetCache(dbConfig.getModuleId(), dbConfig.getType(), strUserId);
        } else if (ObjectUtil.equal(limitConfig, LIMIT_ON)) {
            // 前提条件是： 从轮询队列中找到 轮询到的用户id ?
            // 缓存是必须条件！
            // 1. 检查每小时领取上限 ： 到了就拦截； 否则就往下查询一层
            decrement = stringRedisTemplate.opsForValue().decrement(
                    RedisKeyConstants.M_C_T_U_C_HOUR + "_" + strUserId
            );
            // 2. 如果还有余量，则检查每日上限
            if (Objects.nonNull(decrement) && decrement > 0) {
                decrement = stringRedisTemplate.opsForValue().decrement(
                        RedisKeyConstants.M_C_T_U_C_DAY + "_" + strUserId
                );
                if (Objects.nonNull(decrement) && decrement > 0) {
                    // 3. 如果还有余量，检查每周上限
                    decrement = stringRedisTemplate.opsForValue().decrement(
                            RedisKeyConstants.M_C_T_U_C_WEEK + "_" + strUserId
                    );
                    if (Objects.nonNull(decrement) && decrement > 0) {
                        // 4. 如果还有余量，检查每月上限
                        decrement = stringRedisTemplate.opsForValue().decrement(
                                RedisKeyConstants.M_C_T_U_C_MONTH + "_" + strUserId
                        );
                        if (Objects.nonNull(decrement) && decrement > 0) {
                            // 5. 如果还有余量，检查累计上限
                            decrement = stringRedisTemplate.opsForValue().decrement(
                                    RedisKeyConstants.M_C_T_U_C_TOTAL + "_" + strUserId
                            );
                        }
                    }
                }
            }
        }
        // 这里的逻辑是： 如果分配完了还有余量，表示这个还没到上限呢， 继续插入缓存队列； 如果余量为0， 表示达到某个上限，不在插入缓存队列；
        if (Objects.nonNull(decrement) && decrement > 0) {
            resetCache(dbConfig.getModuleId(), dbConfig.getType(), strUserId);
        }
        return Long.parseLong(strUserId);
    }

    private void resetCache(Long moduleId, Integer type, String strUserId) {
        if (ObjectUtil.equal(T_ORDER, type)) {
            stringRedisTemplate.opsForList().rightPush(
                    RedisKeyConstants.M_C_T_U_Q +  moduleId,
                    String.valueOf(strUserId)
            );
        } else if (ObjectUtil.equal(T_LEAST, type)) {
            stringRedisTemplate.opsForZSet().incrementScore(
                    RedisKeyConstants.M_C_T_U_Q +  moduleId,
                    String.valueOf(strUserId),
                    1
            );
        }
    }


    public String getFromCache(Long moduleId, Integer type) {
        if (ObjectUtil.equal(T_ORDER, type)) {
            return stringRedisTemplate.opsForList().leftPop(
                    RedisKeyConstants.M_C_T_U_Q +  moduleId
            );
        } else if (ObjectUtil.equal(T_LEAST, type)) {
            ZSetOperations.TypedTuple<String> tuple = stringRedisTemplate.opsForZSet().popMax(
                    RedisKeyConstants.M_C_T_U_Q + moduleId
            );
            if (Objects.isNull(tuple)) {
                return null;
            }
            return tuple.getValue();
        }
        return null;
    }

    /**
     * 每天0点1分 自动执行一次， 刷新
     * mod -> 修改为每小时执行一次， 重点在于更新 每小时上限；
     * @since 2024-06-26
     * @author PetePower
     */
    @PostConstruct
    @Scheduled(cron = "0 * * * * *")
    @TenantIgnore
    public void initLocalCache() {
        String value = SpringUtil.getProperty("yudao.job.enable");
        if (!StrUtil.equals("true", value)) {
            return ;
        }
        // 加载配置列表，写入缓存
        List<CrmClueDistributeConfigDO> list = clueDistributeConfigMapper.selectList();
        list.forEach(config -> {
            // 删除 redis
            String key1 = RedisKeyConstants.M_C_T_U_Q + config.getModuleId();
            stringRedisTemplate.delete(key1);
            Integer limitConfig = config.getLimitConfig();
            config.getConfigList().forEach(u -> {
                if (ObjectUtil.equal(limitConfig, LIMIT_ON)) {
                    stringRedisTemplate.delete(RedisKeyConstants.M_C_T_U_C_HOUR + "_" + u.getUserId());
                    stringRedisTemplate.delete(RedisKeyConstants.M_C_T_U_C_DAY + "_" + u.getUserId());
                    stringRedisTemplate.delete(RedisKeyConstants.M_C_T_U_C_WEEK + "_" + u.getUserId());
                    stringRedisTemplate.delete(RedisKeyConstants.M_C_T_U_C_MONTH + "_" + u.getUserId());
                    stringRedisTemplate.delete(RedisKeyConstants.M_C_T_U_C_TOTAL + "_" + u.getUserId());
                }
            });
            if (config.getEnabled()) {
                refreshOne(config);
            }
        });
    }

    public void refreshOne(CrmClueDistributeConfigDO dbConfig) {
        List<CrmClueDistributeConfigRespVO.DistributeConfigVO> configList = dbConfig.getConfigList();
        if (CollUtil.isNotEmpty(configList)) {
            Integer type = dbConfig.getType();
            Integer limitConfig = dbConfig.getLimitConfig();
            // 顺序分配， 使用list
            configList.forEach(config -> {
                // 每日上限
                if (ObjectUtil.equal(limitConfig, LIMIT_ON)) {
                    if (config.getEnable()) {
                        // 设置为0， 可以理解为不设上限
                        Integer hourCountLimit = Objects.isNull(config.getHourCount()) || config.getHourCount() == 0 ? NO_LIMIT : config.getHourCount();
                        // 设置每小时上限
                        stringRedisTemplate.opsForValue().set(
                                RedisKeyConstants.M_C_T_U_C_HOUR  + "_" + config.getUserId(),
                                String.valueOf(hourCountLimit), 1, TimeUnit.HOURS
                        );
                        // 设置每日上限
                        long dayDelta = NO_LIMIT;
                        if (Objects.nonNull(config.getDayCount()) && config.getDayCount() > 0) {
                            Long dayCount = clueMapper.countByAdAndOwner(config.getUserId(), DateUtil.beginOfDay(new Date()).toLocalDateTime());
                            dayDelta = config.getDayCount() - dayCount;
                        }
                        if (dayDelta > 0) {
                            stringRedisTemplate.opsForValue().set(
                                    RedisKeyConstants.M_C_T_U_C_DAY + "_" + config.getUserId(),
                                    String.valueOf(dayDelta)
                            );
                        }
                        // 设置每周上限
                        long weekDelta = NO_LIMIT;
                        if (Objects.nonNull(config.getWeekCount()) && config.getWeekCount() > 0) {
                            Long weekCount = clueMapper.countByAdAndOwner(config.getUserId(), DateUtil.beginOfWeek(new Date()).toLocalDateTime());
                            weekDelta = config.getWeekCount() - weekCount;
                        }
                        if (weekDelta > 0) {
                            stringRedisTemplate.opsForValue().set(
                                    RedisKeyConstants.M_C_T_U_C_WEEK + "_" + config.getUserId(),
                                    String.valueOf(weekDelta)
                            );
                        }
                        // 设置每月上限
                        long monthDelta = NO_LIMIT;
                        if (Objects.nonNull(config.getMonthCount()) && config.getMonthCount() > 0) {
                            Long monthCount = clueMapper.countByAdAndOwner(config.getUserId(), DateUtil.beginOfMonth(new Date()).toLocalDateTime());
                            monthDelta = config.getMonthCount() - monthCount;
                        }
                        if (monthDelta > 0) {
                            stringRedisTemplate.opsForValue().set(
                                    RedisKeyConstants.M_C_T_U_C_MONTH + "_" + config.getUserId(),
                                    String.valueOf(monthDelta)
                            );
                        }
                        // 设置累计上限
                        long totalDelta = NO_LIMIT;
                        if (Objects.nonNull(config.getTotalCount()) && config.getTotalCount() > 0) {
                            Long totalCount = clueMapper.countByAdAndOwner(config.getUserId(), null);
                            totalDelta = config.getTotalCount() - totalCount;
                        }
                        if (totalDelta > 0) {
                            stringRedisTemplate.opsForValue().set(
                                    RedisKeyConstants.M_C_T_U_C_TOTAL + "_" + config.getUserId(),
                                    String.valueOf(totalDelta)
                            );
                        }
                    }
                }
                // 配置顺序分配\或者轮询分配
                if (type == 1) {
                    stringRedisTemplate.opsForList().rightPush(
                            RedisKeyConstants.M_C_T_U_Q + dbConfig.getModuleId(),
                            String.valueOf(config.getUserId())
                    );
                } else if (type == 2) {
                    Long count = clueMapper.countByAdAndOwner(config.getUserId(), null);
                    stringRedisTemplate.opsForZSet().add(
                            RedisKeyConstants.M_C_T_U_Q + dbConfig.getModuleId(),
                            String.valueOf(config.getUserId()),
                            count
                    );
                }
            });
        }
    }
}
